﻿namespace NJQuery {
	using System;
	using System.Web.UI;
	using System.Collections.Generic;
	using System.Collections.Specialized;
	using System.Web.UI.WebControls;
	using System.ComponentModel;
	using System.Globalization;
	using System.Diagnostics.CodeAnalysis;

	/// <summary>
	/// Server control that implements the <see href="http://jqueryui.com/demos/sortable/">jQuery UI Sortable interaction</see> to allow a list of items to be custom sorted by the user.
	/// </summary>
	[ParseChildren(true, "Items")]
	public class SortableList : ManagedWebControl, IPostBackDataHandler {


		#region Constants: Property defaults

		private const int defaultDistance = 1;
		private const int defaultDelay = 0;

		#endregion


		#region Static Methods: Private routines

		private static void CheckIndexes(int[] indexes, out bool sequential, out bool valid) {

			int length = indexes.Length;
			List<int> checkedIndexes = new List<int>(indexes.Length);

			sequential = true;

			for(int i = 0; i < length; i++) {
				int index = indexes[i];

				if(i != index) sequential = false;

				if(index < 0 || index >= length || checkedIndexes.Contains(index)) {
					valid = false;
					return;
				}

				checkedIndexes.Add(index);
			}

			valid = true;
		}

		#endregion


		#region Fields

		private ListItemCollection items;
		private bool resetSortOrder;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="SortableList"/> class.
		/// </summary>
		public SortableList() : base(HtmlTextWriterTag.Ul) { }

		#endregion


		#region Properties: Widget initialization options

		/// <summary>
		/// Time in milliseconds to define when the sorting should start. It helps preventing unwanted drags when clicking on an element.
		/// This property maps directly to the "delay" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultDelay), Category("Behavior")]
		public virtual int Delay {
			get { return ViewState["Delay"] as int? ?? defaultDelay; }
			set { ViewState["Delay"] = value; }
		}

		/// <summary>
		/// Tolerance, in pixels, for when sorting should start. If specified, sorting will not start until after mouse is dragged beyond distance. Can be used to allow for clicks on elements within a handle.
		/// This property maps directly to the "distance" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultDistance), Category("Behavior")]
		public virtual int Distance {
			get { return ViewState["Distance"] as int? ?? defaultDistance; }
			set { ViewState["Distance"] = value; }
		}

		#endregion


		#region Properties: Additional

		/// <summary>
		/// Gets the collection of list items in the list control.
		/// </summary>
		[DefaultValue(null), MergableProperty(false), PersistenceMode(PersistenceMode.InnerDefaultProperty), Category("Misc")]
		public virtual ListItemCollection Items {
			get {
				if(items == null) {
					items = new ListItemCollection();
					if(IsTrackingViewState) ((IStateManager)items).TrackViewState();
				}
				return items;
			}
		}

		#endregion


		#region Properties: Subcontrol

		/// <summary>
		/// Gets the rendered element ID of the hidden field used to receive an updated sort order.
		/// </summary>
		public virtual string SortOrderClientId {
			get { return string.Format(CultureInfo.InvariantCulture, "__{0}_SortOrder", ClientID); }
		}

		#endregion


		#region Events

		/// <summary>
		/// Occurs on postback when the sort order of the <see cref="Items"/> collection was changed on the client.
		/// </summary>
		[Category("Action")]
		public event EventHandler SortOrderChanged;

		#endregion


		#region Methods: Event hooks

		/// <summary>
		/// Raises the <see cref="SortOrderChanged"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object that contains the event data.</param>
		protected virtual void OnSortOrderChanged(EventArgs e) {
			if(SortOrderChanged != null) SortOrderChanged.Invoke(this, e);
		}

		#endregion


		#region Methods: View State

		/// <summary>
		/// Causes the control to track changes to its view state so they can be stored in the object's <see cref="Control.ViewState"/> property.
		/// This method is used primarily by control developers.
		/// </summary>
		protected override void TrackViewState() {
			base.TrackViewState();
			IStateManager manager = items as IStateManager;
			if(manager != null) manager.TrackViewState();
		}

		/// <summary>
		/// Saves any state that was modified after the <see cref="Control.TrackViewState"/> method was invoked.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <returns>An object that represents the control state to round-trip to the client.</returns>
		protected override object SaveViewState() {

			object baseState = base.SaveViewState();

			object itemsState = null;
			IStateManager manager = items as IStateManager;
			if(manager != null) itemsState = manager.SaveViewState();

			if(baseState == null && itemsState == null) return null;

			List<int> selectedState = new List<int>();
			for(int i = 0; i < items.Count; i++)
				if(items[i].Selected) selectedState.Add(i);

			return new Triplet(baseState, itemsState, selectedState.Count == 0 ? null : selectedState.ToArray());
		}

		/// <summary>
		/// Restores view-state information from a previous request that was saved with the <see cref="Control.SaveViewState"/> method.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="savedState">An object that represents the control state to restore.</param>
		protected override void LoadViewState(object savedState) {
			object baseState = null;
			object itemsState = null;
			int[] selectedState = null;

			Triplet triplet = savedState as Triplet;
			if(triplet != null) {
				baseState = triplet.First;
				itemsState = triplet.Second;
				selectedState = triplet.Third as int[];
			}

			base.LoadViewState(baseState);

			if(itemsState != null) {
				IStateManager manager = Items as IStateManager;
				manager.LoadViewState(itemsState);
			}

			//TODO: Decide if Selected Items should be stored in viewstate independently.  If so, use pair
			if(selectedState != null)
				foreach(int index in selectedState)
					if(index >= 0 && index < items.Count)
						items[index].Selected = true;
		}

		#endregion


		#region Methods: Rendering hooks

		/// <summary>
		/// Raises the <see cref="Control.PreRender"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object containing the event data.</param>
		protected override void OnPreRender(EventArgs e) {
			base.OnPreRender(e);

			if(Visible) Page.RegisterRequiresPostBack(this);
		}

		/// <summary>
		/// Allows a jQuery server control to reference scripts and stylesheets necessary to render their client markup and widgets.
		/// </summary>
		/// <param name="manager">The <see cref="Manager"/> used to handle style and script referencing in addition to unified generation of load and ready scripts.</param>
		protected override void Register(Manager manager) {

			manager.RegisterClientScript(ClientScript.Core);
			manager.RegisterClientScript(ClientScript.UICore);
			manager.RegisterClientScript(ClientScript.UIWidgetFactory);
			manager.RegisterClientScript(ClientScript.UIMouseUtility);
			manager.RegisterClientScript(ClientScript.UISortableInteraction);

			manager.RegisterClientStyle(ClientStyle.UICore);
			manager.RegisterTheme();

			manager.RegisterForReadyScript(this);
		}

		/// <summary>
		/// Sends server control content to a provided <see cref="HtmlTextWriter"/> object, which writes the content to be rendered to the client.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A <see cref="HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
		protected override void Render(HtmlTextWriter writer) {

			base.Render(writer);

			ClientScriptManager clientScript = Page.ClientScript;
			clientScript.RegisterForEventValidation(new PostBackOptions(this));

			clientScript.RegisterHiddenField(SortOrderClientId, string.Empty);
		}

		/// <summary>
		/// Adds HTML attributes and styles that need to be rendered to the specified <see cref="HtmlTextWriter"/>.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A <see cref="HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
		protected override void AddAttributesToRender(HtmlTextWriter writer) {

			if(!MasterUseCssStyling) {
				writer.AddStyleAttribute(HtmlTextWriterStyle.ListStyleType, "none");
				writer.AddStyleAttribute(HtmlTextWriterStyle.Padding, "0");
			}

			base.AddAttributesToRender(writer);
		}

		/// <summary>
		/// Renders the contents of the control to the specified writer.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A <see cref="HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
		protected override void RenderContents(HtmlTextWriter writer) {
			base.RenderContents(writer);

			bool useStyleAttributes = !MasterUseCssStyling;
			string idBase = ClientID + ClientIDSeparator;

			for(int i = 0; i < items.Count; i++) {
				ListItem item = items[i];

				string cssClass = "ui-state-default";
				if(!item.Enabled) cssClass += " ui-state-disabled";
				if(item.Selected) cssClass += " ui-state-highlight";

				writer.AddAttribute(HtmlTextWriterAttribute.Class, cssClass);
				writer.AddAttribute(HtmlTextWriterAttribute.Id, idBase + i.ToString(CultureInfo.InvariantCulture));
				if(useStyleAttributes)
					writer.AddStyleAttribute(HtmlTextWriterStyle.PaddingLeft, "1.5em");
				writer.RenderBeginTag(HtmlTextWriterTag.Li);

				if(item.Enabled) {
					writer.AddAttribute(HtmlTextWriterAttribute.Class, "ui-icon ui-icon-arrowthick-2-n-s");
					if(useStyleAttributes) {
						writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "absolute");
						writer.AddStyleAttribute(HtmlTextWriterStyle.MarginLeft, "-1.3em");
					}
					writer.RenderBeginTag(HtmlTextWriterTag.Span);
					writer.RenderEndTag();
				}

				writer.WriteEncodedText(item.Text);

				writer.RenderEndTag();
			}
		}

		/// <summary>
		/// Renders the jQuery "Ready Event" script required to generate the client-side jQuery widget.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderReadyScript(ScriptBuilder script) {
			RenderInitScript(script);
			if(resetSortOrder) RenderSortOrderResetScript(script);
			RenderAdditionalReadyScript(script);
		}

		/// <summary>
		/// Renders the jQuery UI initialization script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderInitScript(ScriptBuilder script) {

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".sortable(");

			PropertyCollection properties = script.CreateProperties();
			AddInitProperties(properties);
			script.RenderProperties(properties);

			script.Append(");");

		}

		protected virtual void RenderSortOrderResetScript(ScriptBuilder script) {

			script.NewLine();

			script.RenderIdSelector(SortOrderClientId);
			script.Append(".val('');");

		}

		/// <summary>
		/// Renders the the "Ready Event" script following the jQuery UI initialization script 
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderAdditionalReadyScript(ScriptBuilder script) {

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".disableSelection();");

			//TODO: Hover Code is Buggy on Client for some reason
			//script.NewLine();

			//script.RenderSelector(ClientID, " li:not(.ui-state-disabled)");
			//script.Append(".hover(function(){$(this).addClass('ui-state-hover');},function(){$(this).removeClass('ui-state-hover');});");

		}

		/// <summary>
		/// Creates the properties object that will be rendered as the jQuery UI initialization options.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="properties">A <see cref="PropertyCollection"/> object that will become the jQuery UI initialization options.</param>
		protected virtual void AddInitProperties(PropertyCollection properties) {

			properties.AddValue("axis", "y");

			properties.AddValue("cancel", ".ui-state-disabled");

			properties.AddValue("cursor", "row-resize");

			int delay = Delay;
			if(delay != defaultDelay) properties.AddValue("delay", delay);

			int distance = Distance;
			if(distance != defaultDistance) properties.AddValue("distance", distance);

			ScriptBuilder updateScript = properties.CreateChildScript();
			RenderUpdateScript(updateScript);
			if(updateScript.Length != 0) properties.AddScript("update", updateScript);

		}

		/// <summary>
		/// Renders the client "update" event script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderUpdateScript(ScriptBuilder script) {

			int startPosition = ClientID.Length + 1;

			script.Append("function(event, ui) {");

			script.Indent++;

			script.NewLine();
			script.Append("var result = '';");

			script.NewLine();
			script.RenderIdSelector(ClientID, " li");
			script.Append(".each(function(index) {");

			script.Indent++;

			script.NewLine();
			script.Append("if(index != 0) result += ',';");

			script.NewLine();
			script.Append("result += $(this).attr('id').substring(");
			script.AppendValue(startPosition);
			script.Append(");");

			script.Indent--;

			script.NewLine();
			script.Append("});");

			script.NewLine();
			script.RenderIdSelector(SortOrderClientId);
			script.Append(".val(result);");

			script.Indent--;
			script.NewLine();

			script.Append("}");
		}

		#endregion


		#region Methods: Post data

		/// <summary>
		/// Processes postback data for an ASP.NET server control.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="postDataKey">The key identifier for the control.</param>
		/// <param name="postCollection">The collection of all incoming name values.</param>
		/// <returns>True if the server control's state changes as a result of the postback; otherwise, false.</returns>
		protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {

			Page.ClientScript.ValidateEvent(postDataKey);

			string sortOrder = postCollection[SortOrderClientId] ?? string.Empty;

			if(sortOrder.Length == 0) return false;

			int[] indexes;
			try {
				indexes = Array.ConvertAll(sortOrder.Split(','), new Converter<string, int>(Convert.ToInt32));
			} catch(FormatException) {
				return false;
			} catch(OverflowException) {
				return false;
			}

			ListItemCollection oldItems = Items;
			if(indexes.Length != oldItems.Count) return false;

			bool sequential;
			bool valid;
			CheckIndexes(indexes, out sequential, out valid);

			//TODO: If it is definitively decided that errors are ignored, then the distinction doesn't matter
			if(sequential || !valid) return false;

			ListItem[] newItems = new ListItem[indexes.Length];
			for(int i = 0; i < indexes.Length; i++)
				newItems[i] = oldItems[indexes[i]];

			items.Clear();
			items.AddRange(newItems);

			resetSortOrder = true;

			return true;
		}

		/// <summary>
		/// Signals the server control to notify the ASP.NET application that the state of the control has changed.
		/// This method is used primarily by control developers.
		/// </summary>
		[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This method is amazing.  You get a complaint if you don't include the protected one, but when you do, it tells you to use events.  I included it simply because it gives the derrived control a chance to hook deeper.")]
		protected virtual void RaisePostDataChangedEvent() {
			OnSortOrderChanged(EventArgs.Empty);
		}

		#endregion


		#region Explicit Implementation: IPostBackDataHandler

		bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) {
			return LoadPostData(postDataKey, postCollection);
		}

		void IPostBackDataHandler.RaisePostDataChangedEvent() {
			RaisePostDataChangedEvent();
		}

		#endregion


	}

}